iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0

目前 PokeThreads 的架構已經有多台 Server 和 Primary / Replica Database:

Database Replication:Primary 與 Read Replica 架構圖

Read 壓力被 Replica 分攤掉了,但仔細想想

這些 Read 真的每一次都需要重新查詢 Database 嗎?

假設現在有 100 萬個使用者,很多人一打開 PokeThreads 第一件事就是看 Feed。回想 Day 3 的內容,光是組出一份 Feed,後端就要做:

找到 Following
      ↓
找到 Posts
      ↓
排序
      ↓
Pagination

如果每一次 Request 都重新跑一次這整套流程:

Request 1 → Database
Request 2 → Database
Request 3 → Database
Request 4 → Database
...

代表大量 Request 其實都在請 Database 重複做很類似的事情。

但有些資料根本不需要每次都重新算,例如:

  • User Profile
  • Post 內容
  • 組好的 Feed
  • 某些 Counter
  • 某些熱門資料

這些資料在一段時間內,可能會被大量使用者反覆讀取。那能不能把這些常用資料,暫時放在一個更適合大量讀取的地方

這就是 Cache(快取)要解決的問題。

比喻:把常查的公式抄成紙條

Cache 可以想成一個放在 Application 與 Database 之間的高速暫存層,用來保存最近常被讀取的資料。

想像 Database 是一本很厚的參考書,裡面收錄了所有資料,但每次要查東西都得翻目錄、找頁碼,很花時間。

如果某個表格或公式三不五時就要查一次,與其每次都整本翻,不如把它抄在一張紙條上,夾在書的最前面,這張紙條就是 Cache,Redis 就是業界很常拿來做這件事的工具。

加上 Cache 之後,架構變成:

加上 Cache 之後的架構圖

這裡會遇到兩種情況:

  • Cache Hit:紙條上剛好抄了這筆資料,直接看紙條就好,不用翻書
  • Cache Miss:紙條上沒抄到,只好翻書查,查到後順便抄一份到紙條上,下次就不用再翻書

Cache-aside

最常見的做法叫 Cache-aside:先查 Cache 有沒有資料,有的話直接回傳;沒有的話(Cache Miss)才去查 Database,查到之後順便寫進 Cache,再回傳給使用者。

Cache-aside 讀寫流程圖

這個做法很直覺,但也有明顯缺點,Cache Miss 時要做三件事:

  1. 查 Cache
  2. 查 DB
  3. 寫回 Cache

這些步驟疊起來,會讓那一次 Request 的延遲明顯變高。

另一個問題更麻煩:

如果 Cache 裡已經有資料,但 Database 被別的操作更新了,Cache 就會變成舊資料(Stale Data)。

延續紙條的比喻:如果書本身被修訂了(例如出版社發了勘誤表,公式改版),但你紙條上抄的還是舊版內容,你看紙條的時候完全不會知道書已經改了,只會照著舊公式繼續算下去。

假設 Pikachu 的暱稱原本是「黃色老鼠」,他改成「皮神」,但 Cache 這張紙條上還留著「黃色老鼠」,下一次 Request 讀到的還是紙條上的舊資料。

用 TTL 強制更新

最簡單的處理方式是幫紙條標上「有效期限」,設定 TTL(Time To Live)

User Profile
TTL = 5 minutes

五分鐘後這筆 Cache Entry 自動失效,下一次 Request 就會直接查 Database,順便把 Cache 更新成最新資料。

TTL 能降低「Cache 永遠是舊資料」的風險,但不能保證 Cache 隨時都跟 Database 同步,例如:

10:00  Database 資料更新
10:01  Cache 仍保有舊資料
10:04  Cache 仍保有舊資料
10:05  TTL 到期,下次 Request 才會更新

在 TTL 到期之前,使用者仍然有可能讀到舊資料。

Write-through

如果希望紙條跟書本隨時保持一致,可以改用 Write-through

寫入資料時,先更新 Cache 這張紙條,再由 Cache 同步寫回 Database 這本書,等 Database 也確認寫入完成後才回傳成功。

這種做法的 Write 會比較慢,因為要等兩邊都寫完,但換來的好處是後續的 Read 都能拿到最新資料。

實務上,Cache-aside 和 Write-through 通常不是二選一,而是「讀寫分工」:

  • Read 用 Cache-aside
  • Write 用 Write-through

在效能與一致性之間找平衡。

Write-back

Write-back(Write-behind) 則是另一個極端:先直接在紙條上塗改,立刻回報「改好了」,並把這張紙條標記成 Dirty(代表跟書本內容還不一致),等到某個時機點(例如定時器觸發、系統空閒)才批次把 Dirty 的內容謄回 Database 這本書。

這種做法寫入速度非常快,但風險也最高

如果 Cache 在資料還沒寫回 Database 之前就掛掉,這筆資料就永遠遺失了。

實作難度也比前兩種高上不少。

哪些資料值得被 Cache?

不是所有資料都適合放進 Cache,可以從三個角度評估:

  1. 讀取頻率:很少被讀的資料,Cache 帶來的效益有限
  2. 更新頻率:資料變動越頻繁,Cache Invalidation 就越麻煩
  3. 能否接受 Stale Data:有些資料稍微過時也不影響使用

除此之外,還有一個常被忽略的問題:如果 Cache 本身掛掉了怎麼辦?

這代表所有 Request 會瞬間全部打回 Database,如果 Database 承受不住這波流量,很可能引發連鎖故障,這種現象有時被稱為 Cache Avalanche(快取雪崩),設計時需要考慮 Cache 失效或重啟時的保護機制。

小結

加上 Cache 之後,PokeThreads 的讀取路徑變快了很多,但也多了新的東西要顧:

  • Cache Hit / Cache Miss
  • TTL 該設多久
  • Cache Invalidation
  • Stale Data
  • Cache 本身故障的風險

這正好印證一件事:System Design 不是不斷疊加元件就結束了,而是每解決一個問題,就會帶來一組新的問題要權衡,這就是 Trade-off。

目前 Cache 加速的都是文字類型的資料,如果 PokeThreads 之後想讓使用者上傳圖片、影片呢?這些檔案動輒好幾 MB,直接塞進 Database 或 Cache 都不是好主意,這是接下來要處理的問題。


上一篇
Day 7 一顆 Database 不夠用:Database Replication
下一篇
Day 9 運用 CDN 處理多媒體素材
系列文
系統設計就像九頭蛇:打造社群網站的 30 天10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言